/*
 ============================================================================
 Name        : Gallifrey.c
 Author      : Aleksandar Stancic 09/08
 Description : Implementacija NTP server/klijenta
 ============================================================================
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include "portutil/sockets.h"
#include "portutil/threading.h"

#include "ntp/types.h"
#include "ntp/ntppacket.h"
#include "ntp/ntputil.h"

#include "gf.h"

#include "portutil/timing.h"

#define NEAR_THRESHOLD (100)

int abs(int x)
{
	return (x < 0) ? -x : x;
}

// Citanje konfiguracionog fajla sa spiskom servera
int parseConfig(const char * fileName, GFparams * gp)
{
	FILE *f = fopen(fileName, "rt");
	if(f == NULL)
		return 0;
	else
	{
		gp->serverCount = 0;
		while(!feof(f))
		{
			char lineBuffer[128] = {0};
			char serverBuffer[128] = {0};
			fgets(lineBuffer, 128, f);

			// Citanje se obavlje po uzoru na oblik u kom je /etc/ntp.conf fajl
			// referentrne NTP implementacije za Linux/Unix
			if(lineBuffer[0] == 's' && sscanf(lineBuffer, "server %s", serverBuffer) == 1)
			{
				strcpy(gp->hostnames[gp->serverCount], serverBuffer);
				gp->ipTxt[gp->serverCount][0] = '\0';
				gp->ip[gp->serverCount] = 0;
				gp->gotResponse[gp->serverCount] = 0;
				gp->serverCount++;
			}
		}

		fclose(f);
		return gp->serverCount;
	}
}

// Rezolucija hostnameova u IP adrese kroz DNS
int resolveServers(GFparams * gf)
{
	int i, resolved = 0;
	for(i = 0; i < gf->serverCount; ++i)
	{
		// Mozda je vec IP adresa u pitanju
		unsigned int ip = cu_textToIP(gf->hostnames[i]);

		if(ip == 0)		// Ako nije, pokusaj resolve
		{
			ip = cu_resolveDomain(gf->hostnames[i]);
			gf->ip[i] = ip;
			if(ip > 0)	// Ako je uspjelo, zabiljezi
			{
				cu_IPtoText(ip, gf->ipTxt[i]);
				resolved++;
			}
		}
		else
		{
			gf->ip[i] = ip;
			cu_IPtoText(ip, gf->ipTxt[i]);
			resolved++;
		}
	}
	return resolved;
}

// Kontaktiranje svih servera koji su ucitani i za koje su dostupne IP adrese
int queryServers(GFparams * gf)
{
	int i, queries = 0;
	NTPpack *query = ntp_createPacket();
	CUSocket *qsock = cu_createUDPSocket(0);
	if(qsock == NULL) return 0;

	for(i = 0; i < gf->serverCount; ++i)
	{
		if(gf->ip[i] > 0)	// Ako imamo IP adresu, mozda resolve nije uspio
		{
			int snd = 0, rcv = 0;
			unsigned int rcvIP = 0;
			CUSecTime start, finish;

			start = cu_getSecTime();	// Zabiljezi pocetno vrijeme radi
										// racunanja round-tripa
			// Upisi trenutno vrijeme u paket
			query->transmitTimestamp = ntp_getCurrentTime();

			// Slanje paketa UDP-om do servera
			snd = cu_sendDataToIP(qsock, (const char *)query, sizeof(NTPpack),
					gf->ip[i], NTP_PORT);
			if(snd < sizeof(NTPpack)) continue;	// Slanje nije uspjelo

			// Prijem paketa
			rcv = cu_recvDataFrom(qsock, (char *)&gf->lastResponse[i], sizeof(NTPpack),
					&rcvIP, NULL, NULL);
			finish = cu_getSecTime();	// Zabiljezi vrijeme pristizanja

			// Samo sekunde u minuti su vracene
			if(finish.seconds < start.seconds) finish.seconds += 60;

			// Posto je UDP u pitanju, treba provjeriti da li je stigao sa
			// prave adrese i po timestampovima uporediti da li je to bas
			// odgovor na upit koji je poslan
			if(rcv >= sizeof(NTPpack) && rcvIP == gf->ip[i] &&
					gf->lastResponse[i].origTimestamp.seconds == query->transmitTimestamp.seconds &&
					gf->lastResponse[i].origTimestamp.fraction == query->transmitTimestamp.fraction)
			{
				Uint32 secR = 0, msR = 0;
				Uint32 secL = 0, msL = 0;

				gf->gotResponse[i] = 1;

				// Vrijeme round-tripa u milisekundama
				gf->roundtrip[i] = ((int)finish.seconds - (int)start.seconds) * 1000
						+ ((int)finish.microSeconds - (int)start.microSeconds) / 1000;

				// Theta - polovina vremena koje je paket proveo na serveru i vremena
				// koji je ovaj klijent cekao na odgovor
				gf->theta[i] = gf->roundtrip[i] / 2 +
						(gf->lastResponse[i].transmitTimestamp.seconds -
								gf->lastResponse[i].recvTimestamp.seconds) * 1000 / 2 +
						((ntp_getMiliseconds(&(gf->lastResponse[i].transmitTimestamp))) -
								(ntp_getMiliseconds(&(gf->lastResponse[i].transmitTimestamp)))) / 2;

				secR = ntp_reverseBytes32(gf->lastResponse[i].recvTimestamp.seconds);
				msR = ntp_getMiliseconds(&(gf->lastResponse[i].recvTimestamp)) - gf->theta[i];

				while(msR < 0)
				{
					secR--;
					msR += 1000;
				}

				// Na osnovu timestampova i thete se racuna delta vremena
				// na serveru i lokalnog
				secL = ntp_reverseBytes32(gf->lastResponse[i].origTimestamp.seconds);
				msL = ntp_getMiliseconds(&(gf->lastResponse[i].origTimestamp));

				if(secR > secL)
					gf->delta[i] = (secR - secL) * 1000;
				else
					gf->delta[i] = (int)(secL - secR) * -1000;

				// Konacna delta u milisekundama
				gf->delta[i] += (msR - msL);

				queries++;
			}
			else
			{
				gf->gotResponse[i] = 0;
			}
		}
	}
	free(query);
	return queries;
}

// Ispis svih dobijenih rezultata upita
void dumpQueryResults(GFparams * gp)
{
	int i;
	for(i = 0; i < gp->serverCount; ++i)
	{
		printf("[%s]\n", gp->hostnames[i]);
		if(gp->gotResponse[i])
		{
			printf("Roundtrip: %d ms\n", gp->roundtrip[i]);
			printf("Theta: %d ms\n", gp->theta[i]);
			printf("Delta: %d ms\n", gp->delta[i]);
			ntp_packetInfo(&(gp->lastResponse[i]));
		}
		else
		{
			printf("No response\n\n");
		}
	}
}

// Procjena korekcije lokalnog sata na osnovu izracunatih delti
int computeDelta(GFparams * gp)
{
	int deltas[MAX_SERVERS];
	int count = 0;
	int i, j, tmp;
	int median = 0;
	int nSum = 0;
	int nCount = 0;

	for(i = 0; i < gp->serverCount; ++i)
	{
		// U igri su samo serveri od kojih je stigao odgovor
		if(gp->gotResponse[i])
		{
			deltas[count++] = gp->delta[i];
		}
	}

	if(count == 0) return 0;

	// Sortiranje dobijenih delti u rastucem poretku
	for(i = 0; i < count - 1; ++i)
	{
		for(j = i + 1; j < count; ++j)
		{
			if(deltas[i] > deltas[j])
			{
				tmp = deltas[i];
				deltas[i] = deltas[j];
				deltas[j] = tmp;
			}
		}
	}

	// Median vrijednost u skupu delti
	median = deltas[(count - 1) / 2];

	// Sabiru se i sve vrijednosti koje su unutar nekog kruga
	// oko mediane (defaultno 100 ms)
	for(i = 0; i < count; ++i)
	{
		if(abs(median - deltas[i]) <= NEAR_THRESHOLD)
		{
			nSum += deltas[i];
			nCount++;
		}
	}

	// Konacna procjena je prosjek odabranih vrijednosti
	return nSum / nCount;
}

int main(void)
{
	GFparams gp;

	int srvLoaded = 0, srvResolved = 0, srvQueried = 0;
	memset(&gp, 0, sizeof(GFparams));
	cu_initTiming();

	srvLoaded = parseConfig("ntp-servers.txt", &gp);
#ifndef WIN32
	// Ako je Linux/Unix u pitanju i lokalne konfiguracije nema,
	// pokusaj ucitati konfiguraciju NTP-a, ako je ima
	if(srvLoaded == 0)
		srvLoaded = parseConfig("/etc/ntp.conf", &gp);
#endif

	srvResolved = resolveServers(&gp);	// Rezolucija domena
	srvQueried = queryServers(&gp);	// Slanje upita

	printf("Servers: %d loaded from config, %d resolved, %d queried\n\n",
			srvLoaded, srvResolved, srvQueried);

	dumpQueryResults(&gp);	// Ispis rezultata

	printf("\n\nComputed correction : %+d ms\n", computeDelta(&gp));

	return EXIT_SUCCESS;
}
